<?php

namespace src;

use Closure;
use ReflectionClass;
use ReflectionException;
use ReflectionNamedType;
use ReflectionParameter;
use src\exceptions\BindingResolutionException;
use src\exceptions\LogicException;
use src\interfaces\IContainer;

class Container implements IContainer, \ArrayAccess 
{
    protected static $instance;
    protected $resolved = [];
    protected $bindings = [];
    protected $instances = [];
    protected $aliases = [];
    protected $abstractAliases = [];

    protected $buildStack = [];
    protected $with = [];


    public function bound($abstract)
    {
        return isset($this->instances[$abstract]) ||
            isset($this->bindings[$abstract]) ||
            $this->isAlias($abstract);
    }

    //是否定义别名
    private function isAlias($name)
    {
        return isset($this->aliases[$name]);
    }

    /**
     * @throws \Exception
     */
    public function alias($abstract, $alias)
    {
        if($abstract == $alias){
            throw new LogicException('别名绑定不能是自己');
        }
        $this->aliases[$alias] = $abstract;
        $this->abstractAliases[$abstract][] = $alias;
    }

    public function bind($abstract, $concrete = null, $shared = false)
    {
        //删除实例 和删除别名
        unset($this->instances[$abstract], $this->aliases[$abstract]);
        if (is_null($concrete)) {
            $concrete = $abstract;
        }
        if (!$concrete instanceof Closure){
            $concrete = $this->getClosure($abstract, $concrete);
        }
        $this->bindings[$abstract] = compact('concrete', 'shared');
        //如果解析过 重新绑定
        if ($this->resolved($abstract)){
            $this->rebound($abstract);
        }
    }

    private function rebound($abstract)
    {
//        $instance = $this->make($abstract);
    }

    //是否解析过
    protected function resolved($abstract)
    {
        if ($this->isAlias($abstract)){
            $abstract = $this->getAlias($abstract);
        }
        return isset($this->resolved[$abstract]) ||
            isset($this->instances[$abstract]);
    }

    // 获取抽象的别名 如果是别名返回
    private function getAlias($abstract)
    {
        return isset($this->aliases[$abstract]) ?
            $this->getAlias($this->aliases[$abstract]) :
            $abstract;
    }

    protected function getClosure($abstract, $concrete)
    {
        return function ($container, $parameters = []) use ($abstract, $concrete){
            if ($abstract == $concrete){
                return $container->build($abstract);
            }else{
                return $container->resolve($concrete, $parameters);
            }
        };
    }

    public function singleton($abstract, $concrete = null)
    {
        $this->bind($abstract, $concrete, true);
    }

    public function instance($abstract, $instance)
    {
        $this->instances[$abstract] = $instance;
    }

    /**
     * @throws BindingResolutionException
     */
    public function make($abstract, array $parameters = [])
    {
        return $this->resolve($abstract, $parameters);
    }

    /**
     * @throws BindingResolutionException
     */
    protected function resolve($abstract, array $parameters = [])
    {
        $abstract = $this->getAlias($abstract);
        if (isset($this->instances[$abstract])){
            return $this->instances[$abstract];
        }
        $this->with[] = $parameters;
        $concrete = $this->getConcrete($abstract);
        if ($this->isBuildable($concrete, $abstract)){
            $object = $this->build($concrete);
        }else{
            $object = $this->make($abstract);
        }
        if ($this->isShared($abstract)) {
            $this->instances[$abstract] = $object;
        }
        $this->resolved[$abstract] = true;
        array_pop($this->with);
        return $object;
    }

    private function isShared($abstract)
    {
        $shared = $this->bindings[$abstract]['shared'] ?? false;
        return isset($this->instances[$abstract]) || $shared === true;
    }

    /**
     * @throws BindingResolutionException
     */
    private function build($concrete)
    {
        if ($concrete instanceof Closure){
            return $concrete($this, $this->getLastParameterOverride());
        }
        try {
            $reflector = new ReflectionClass($concrete);
        }catch (ReflectionException $e){
            throw new BindingResolutionException("Target class [$concrete] 不存在.", 0, $e);
        }
        if (!$reflector->isInstantiable()){
            throw new BindingResolutionException("Target class [$concrete] 不能实例化.", 0);
        }
        $this->buildStack[] = $concrete;
        $constructor = $reflector->getConstructor();
        if (is_null($constructor)) {
            array_pop($this->buildStack);
            return new $concrete;
        }
        $dependencies = $constructor->getParameters();
        $instances = $this->resolveDependencies($dependencies);
        array_pop($this->buildStack);
        return $reflector->newInstanceArgs($instances);
    }

    private function resolveDependencies(array $dependencies)
    {
        $results = [];
        foreach ($dependencies as $dependency){
            if ($this->hasParameterOverride($dependency)){
                $results[] = $this->getParameterOverride($dependency->getName());
                continue;
            }
            $result = is_null($this->getParameterClassName($dependency)) ?
            $this->resolveNonClass($dependency) : $this->resolveClass($dependency);
            $results[] = $result;
        }
        return $results;
    }

    private function resolveClass(ReflectionParameter $parameter)
    {
        return $this->make($parameter->getClass()->name);
    }

    private function resolveNonClass(ReflectionParameter $parameter)
    {
        if ($parameter->isDefaultValueAvailable()){
            return $parameter->getDefaultValue();
        }
        $message = "Unresolvable dependency resolving [$parameter] in class {$parameter->getDeclaringClass()->getName()}";
        throw new BindingResolutionException($message);
    }

    private function getParameterClassName(\ReflectionParameter $parameter)
    {
        $type = $parameter->getType();
        if (! $type instanceof ReflectionNamedType || $type->isBuiltin()) {
            return null;
        }
        $name = $type->getName();
        if (!is_null($class = $parameter->getDeclaringClass())){
            if ($name === 'self'){
                return  $class->getName();
            }
            if ($name === 'parent' && $parent = $class->getParentClass()) {
                return $parent->getName();
            }
        }
        return $name;
    }

    private function hasParameterOverride($dependency)
    {
        return array_key_exists(
            $dependency->name, $this->getLastParameterOverride()
        );
    }

    private function getParameterOverride($dependency)
    {
        return $this->getLastParameterOverride()[$dependency];
    }

    private function getLastParameterOverride()
    {
        return count($this->with) ? end($this->with) : [];
    }

    private function isBuildable($concrete, $abstract)
    {
        return $concrete == $abstract || $concrete instanceof Closure;
    }

    private function getConcrete($abstract)
    {
        if (isset($this->bindings[$abstract])){
            return $this->bindings[$abstract]['concrete'];
        }
        return $abstract;
    }


    public function offsetExists($key)
    {
        return $this->bound($key);
    }

    public function offsetGet($key)
    {
        return $this->make($key);
    }

    public function offsetSet($key, $value)
    {
        $this->bind($key, $value instanceof Closure ? $value : function () use ($value) {
            return $value;
        });    }

    public function offsetUnset($key)
    {
        unset($this->bindings[$key], $this->instances[$key], $this->resolved[$key]);
    }
}